// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Text;

using System.Threading;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Transactions;
using System.Data.SqlClient;
using System.Data;

using System.IO;
using System.Diagnostics;
using Microsoft.TeamFoundation.Migration.Toolkit;
using Microsoft.TeamFoundation.Migration.Toolkit.VC;
using System.Globalization;
using Microsoft.Teamfoundation.Migration.VersionControl.SharePoint.Versions;
using System.Xml;
using System.Collections;
using Microsoft.TeamFoundation.Client;

namespace Microsoft.TeamFoundation.Migration.VersionControl.Wss
{
    public class WssToTfsAnalysisEngine
    {
        public WssToTfsAnalysisEngine(VersionControlSession session, ChangeGroupingMananger manager)
        {
            Debug.Assert(manager.Direction == Direction);

            if (manager.Direction != Direction)
            {
                throw new MigrationException("The grouping manager direction must be SourceToTfs");
            }

            m_manager = manager;
            m_session = session;

            m_skipComment = m_session.GetValue<string>("SkipChangeComment", null);
            m_commentModifier = session.GetValue<string>("MigrationChangeCommentModifier", "(Migrated by TFS Migration Toolkit)");
        }

        public void Run()
        {
            using (TraceManager.StartLogicalOperation("Analyze"))
            {
                AnalysisVersionsByMapping((Session)m_session);
            }
        }

        private void createActionGroupForRevision(IMigrationTransaction trx,
            DocumentVersion version,
            string tfsServerPath,
            string wssCanonicalPath,
            ChangeAction changeType,
            int fileId)
        {
            ChangeGrouping grouping = m_manager.Create(string.Format(CultureInfo.InvariantCulture, "{0}#{1}#{2}", fileId, version.Version, wssCanonicalPath));

            grouping.Comment = version.Comment;

            if (string.IsNullOrEmpty(version.CreatedBy))
            {
                TeamFoundationServer tfsServer = TeamFoundationServerFactory.GetServer(MigrationConfiguration.TfsServers[m_session.Target.Server].Server);
                grouping.Owner = tfsServer.AuthenticatedUserName;
            }
            else
            {
                grouping.Owner = version.CreatedBy;
            }
            grouping.ChangeTimeUtc = applyGmtOffset(version.Created);
            grouping.SessionId = m_session.Id;

            string downloadUrl = (version.IsLatest) ? null : version.Url;

            IMigrationAction action = grouping.CreateAction();
            action.Action = changeType;
            action.TargetTargetItem = new TfsMigrationItem(null, tfsServerPath, m_session);
            action.SourceItem = new WssMigrationItem(fileId, wssCanonicalPath, version.Version, downloadUrl);

            grouping.AddAction(action);

            string tentativeLocalPath = TfsUtil.GetLocalPathForServerItem(tfsServerPath, m_session);
            validatePotentialLocalPath(tentativeLocalPath);

            grouping.Save(trx);

            TraceManager.WriteLine(TraceManager.Engine, "Created {0} action with id {1}", changeType, grouping.ChangeGroupId);
        }

        private DateTime applyGmtOffset(DateTime dateTime)
        {
            return new DateTime(dateTime.Add(GmtOffset).Ticks, DateTimeKind.Utc);
        }

        private static void validatePotentialLocalPath(string tentativeLocalPath)
        {
            try
            {
                // the local is kept around to prevent the call from being optimized away.

                string resultingPath = Path.GetFullPath(tentativeLocalPath);
                Debug.Assert(resultingPath != null);
            }
            catch (PathTooLongException)
            {
                Trace.TraceWarning("An item being migrated will have too long of a path to successfully migrate.  Shorten the target workspace root directory to work-around this issue: {0}", tentativeLocalPath);
            }
        }

        private void AnalysisVersionsByMapping(Session session)
        {
            WSSListEnumerator le = new WSSListEnumerator(DocumentLibrary);
            ListItemChangeInfo changes = le.GetChanges(session,
                delegate(ListItemInfo info)
                {
                    return (getTfsServerPath(info.FilePath) != null);
                });

            if (0 < (changes.Adds.Count + changes.Edits.Count + changes.Deletes.Count + changes.Renames.Count))
            {
                using(IMigrationTransaction trx = DataAccessManager.Current.StartTransaction())
                {
                    MigrationSqlTransaction sqlTrx = (MigrationSqlTransaction)trx;

                    processListChanges(sqlTrx, changes);
                    promoteCurrentListToMasterList(sqlTrx);
                    m_manager.PromoteAnalysisToPending(sqlTrx);

                    trx.Complete();
                }
            }
            else
            {
                TraceManager.TraceInformation("There were no new WSS changes found during analysis");
            }
        }

        private void processListChanges(IMigrationTransaction trx, ListItemChangeInfo changes)
        {
            processDeletes(trx, changes.Deletes);
            processAdds(trx, changes.Adds);
            processEdits(trx, changes.Edits);
            processRenames(trx, changes.Renames);
        }

        private void processRenames(IMigrationTransaction trx, List<RenameListItemInfo> list)
        {
            // The order for rename change group
            // 1. One Group for all file renames.
            // 2. One group for each edits if there is a rename+edit change Or one add and one delete group for each folder rename
            List<RenameListItemInfo> folderRenamelist = new List<RenameListItemInfo>();
            Dictionary<DocumentVersion, RenameListItemInfo> editList= new Dictionary<DocumentVersion, RenameListItemInfo>();
            list.Sort(compareRenameActionByFilePath);

            ChangeGrouping fileRenameGroup = m_manager.Create(WSS2TFSResources.WssRenameGroupName);
            fileRenameGroup.Owner = m_session.Target.TfsClient.AuthenticatedUser;
            fileRenameGroup.Comment = WSS2TFSResources.WsslRenameGroupComment;
            fileRenameGroup.SessionId = m_session.Id;
            fileRenameGroup.ChangeTimeUtc = DateTime.UtcNow;
            bool fileRenameGroupMade = false;
            
            foreach (RenameListItemInfo rli in list)
            {

                if (rli.FSObjType == 0) //file rename
                {
                    Document d = getWssDocumentFromListItem(rli);

                    int latestRev = 1;

                    foreach (DocumentVersion v in d.Versions)
                    {
                        /* the rename must be handled first
                         * normally we would also call isMigrationCandidate but renames persist the comment
                         * of the previous action so we run into a problem where if an item is migrated from
                         * TFS to WSS it has the migration comment in it (and should be skipped), not if you
                         * rename in WSS the renamed item has the migration comment and will be skipped.
                         */
                        if (v.IsLatest)
                        {
                            latestRev = v.Version;

                            string wssCanonicalPath = getCanonicalUrlFromListItem(rli.FilePath);

                            IMigrationAction renameAction = fileRenameGroup.CreateAction();

                            renameAction.SourceItem = new WssMigrationItem(wssCanonicalPath, v.Version);
                            renameAction.TargetSourceItem = new TfsMigrationItem(null, getTfsServerPath(rli.OldFilePath), m_session);
                            renameAction.TargetTargetItem = new TfsMigrationItem(null, getTfsServerPath(rli.FilePath), m_session);
                            renameAction.Action = ChangeAction.Rename;

                            fileRenameGroup.AddAction(renameAction);
                            fileRenameGroupMade = true;
                        }
                    }

                    // renames alone do not produce a revision entry
                    // so multiple revs means we have edits too.
                    // one edit will be a no-op (the rename)
                    if (latestRev > rli.PreviousVersion)
                    {
                        foreach (DocumentVersion v in d.Versions)
                        {
                            if (v.Version > rli.PreviousVersion)
                            {
                                editList.Add(v, rli);
                            }
                        }
                    }
                }
                else
                {
                    folderRenamelist.Add(rli);
                }
            }
            if (fileRenameGroupMade)
            {
                fileRenameGroup.Save(trx);
            }
            TraceManager.WriteLine(TraceManager.Engine, "Created rename action with id {0}", fileRenameGroup.ChangeGroupId);

            foreach (KeyValuePair<DocumentVersion, RenameListItemInfo> editChange in editList)
            {
                processEdit(trx, editChange.Value, editChange.Key);
            }

            foreach (RenameListItemInfo rli in folderRenamelist)
            {
                if (rli.FSObjType != 0) //file rename
                {
                    string wssCanonicalPath = getCanonicalUrlFromListItem(rli.FilePath);
                    ChangeGrouping group = m_manager.Create(string.Format(CultureInfo.InvariantCulture, "{0}#{1}#{2}", rli.FileId, 1, wssCanonicalPath));
                    group.Owner = m_session.Target.TfsClient.AuthenticatedUser;
                    group.ChangeTimeUtc = applyGmtOffset(rli.Modified);
                    group.SessionId = m_session.Id;
                    // Split folder rename into folder delete and folder add
                    IMigrationAction folderDeleteAction = group.CreateAction();
                    folderDeleteAction.Action = ChangeAction.Delete;
                    folderDeleteAction.TargetTargetItem = new TfsMigrationItem(null, getTfsServerPath(rli.OldFilePath), m_session);
                    group.AddAction(folderDeleteAction);
                    IMigrationAction folderAddAction = group.CreateAction();
                    folderAddAction.SourceItem = new WssMigrationItem(ensureEndsWithSlash(wssCanonicalPath), 1);
                    folderAddAction.TargetTargetItem = new TfsMigrationItem(null, ensureEndsWithSlash(getTfsServerPath(rli.FilePath)), m_session);
                    folderAddAction.Action = ChangeAction.Add;
                    group.AddAction(folderAddAction);
                    group.Save(trx);

                    TraceManager.WriteLine(TraceManager.Engine, "Created rename action with id {0}", group.ChangeGroupId);
                }
            }
        }

        /// <summary>
        /// compare old path length of two rename actions. If x's source path length is smaller, x is larger than y.
        /// </summary>
        /// <param name="x">first renamelistitem to be compared</param>
        /// <param name="y">second renamelistitem to be compared</param>
        /// <returns>positive value if x's source length is smaller. negative value if y's source length is smaller.
        /// 0 if length is equal or either one is null.</returns>
        private static int compareRenameActionByFilePath(RenameListItemInfo x, RenameListItemInfo y)
        {
            if ((x == null) || (y == null))
            {
                return 0;
            }
            return (y.OldFilePath.Length - x.OldFilePath.Length);
        }

        private void processEdits(IMigrationTransaction trx, List<ListItemInfo> list)
        {
            foreach (ListItemInfo li in list)
            {
                Document d = getWssDocumentFromListItem(li);

                foreach (DocumentVersion v in d.Versions)
                {
                    processEdit(trx, li, v);
                }
            }
        }

        private void processEdit(IMigrationTransaction trx, ListItemInfo li, DocumentVersion v)
        {
            if (isMigrationCandidate(v))
            {
                if (v.Version > li.PreviousVersion)
                {
                    string wssCanonicalPath = getCanonicalUrlFromListItem(li.FilePath);
                    string tfsServerPath = getTfsServerPath(li.FilePath);

                    createActionGroupForRevision(trx, v, tfsServerPath, wssCanonicalPath, ChangeAction.Edit, li.FileId);
                }
            }

        }


        private void processAdds(IMigrationTransaction trx, List<ListItemInfo> list)
        {
            foreach (ListItemInfo li in list)
            {
                if (li.FSObjType == 0) // file
                {
                    Document d = getWssDocumentFromListItem(li);

                    ChangeAction action = ChangeAction.Add;
                    foreach (DocumentVersion v in d.Versions)
                    {
                        if (isMigrationCandidate(v))
                        {
                            string wssCanonicalPath = getCanonicalUrlFromListItem(li.FilePath);
                            string tfsServerPath = getTfsServerPath(li.FilePath);

                            createActionGroupForRevision(trx, v, tfsServerPath, wssCanonicalPath, action, li.FileId);
                        }

                        action = ChangeAction.Edit;
                    }
                }
                else // folder
                {
                    string wssCanonicalPath = getCanonicalUrlFromListItem(li.FilePath);
                    string tfsServerPath = getTfsServerPath(li.FilePath);
                    ChangeGrouping group = m_manager.Create(string.Format(CultureInfo.InvariantCulture, "{0}#{1}#{2}", li.FileId, 1, ensureEndsWithSlash(wssCanonicalPath)));
                    group.Owner = m_session.Target.TfsClient.AuthenticatedUser;
                    group.ChangeTimeUtc = applyGmtOffset(li.Modified);
                    group.SessionId = m_session.Id;
                    IMigrationAction folderAddAction = group.CreateAction();
                    folderAddAction.Action = ChangeAction.Add;
                    folderAddAction.TargetTargetItem = new TfsMigrationItem(null, ensureEndsWithSlash(tfsServerPath), m_session);
                    folderAddAction.SourceItem = new WssMigrationItem(li.FileId, ensureEndsWithSlash(wssCanonicalPath), 1, null);
                    group.AddAction(folderAddAction);
                    group.Save(trx);

                    TraceManager.WriteLine(TraceManager.Engine, "Created add action with id {0}", group.ChangeGroupId);
                }
            }
        }

        string getCanonicalUrlFromListItem(string filePath)
        {
            string docLib = ensureEndsWithSlash(DocumentLibrary);

            if (!filePath.StartsWith(
                docLib, 
                StringComparison.InvariantCultureIgnoreCase))
            {
                // we are dealing with a subsite
                filePath = stripSubsite(BaseUrl, filePath);
            }

            return string.Format(CultureInfo.InvariantCulture, "{0}{1}", ensureEndsWithSlash(BaseUrl), filePath);
        }

        private static string ensureEndsWithSlash(string uriPath)
        {
            if (!uriPath.EndsWith("/"))
            {
                return uriPath + '/';
            }

            return uriPath;
        }

        private static string stripSubsite(string baseUrl, string filePath)
        {
            Uri u = new Uri(baseUrl);

            // add the trailing slash to strip it too
            string subsite = u.GetComponents(UriComponents.Path, UriFormat.Unescaped);

            // if there is no subsite then we're done
            if (!string.IsNullOrEmpty(subsite))
            {
                if (string.Compare(filePath, subsite, StringComparison.InvariantCultureIgnoreCase) == 0)  
                {
                    return string.Empty;
                } 
                if (!subsite.EndsWith("/") && subsite.Length < filePath.Length)
                {
                    subsite += "/";
                }

                if (filePath.StartsWith(
                    subsite,
                    StringComparison.InvariantCultureIgnoreCase))
                {
                    return filePath.Remove(0, subsite.Length);
                }
                else
                {
                    throw new ArgumentException(WSS2TFSResources.InvalidUrl);
                }
            }
            else
            {
                return filePath;
            }
        }

        Document getWssDocumentFromListItem(ListItemInfo li)
        {
            string canonicalUrl = getCanonicalUrlFromListItem(li.FilePath);
            using (Versions vers = Util.GetVersionsServiceForUrl(canonicalUrl, m_session))
            {

                if (vers != null)
                {
                    XmlNode versions = null;

                    versions = vers.GetVersions(canonicalUrl);

                    if (versions != null)
                    {
                        return new Document(versions, ((Session)m_session).Culture );
                    }
                }
            }

            throw new MigrationException("Unable to get document history");
        }

        private void processDeletes(IMigrationTransaction trx, List<ListItemInfo> list)
        {
            foreach (ListItemInfo li in list)
            {
                ChangeGrouping group = m_manager.Create(string.Format(CultureInfo.InvariantCulture, "{0}#{1}#{2}", li.FileId, 0, li.FilePath));
                group.Comment = "Item deleted from WSS";
                group.Owner = Environment.UserName;
                group.SessionId = m_session.Id;
                group.ChangeTimeUtc = DateTime.UtcNow;

                IMigrationAction deleteAction = group.CreateAction();
                deleteAction.TargetTargetItem = new TfsMigrationItem(null, getTfsServerPath(li.FilePath), m_session);
                deleteAction.Action = ChangeAction.Delete;

                group.AddAction(deleteAction);

                group.Save(trx);

                TraceManager.WriteLine(TraceManager.Engine, "Created delete action with id {0}", group.ChangeGroupId);
            }
        }

        private string getTfsServerPath(string filePath)
        {
            string mappedPath = null;

            VersionControlMapping m = Util.FindAppropriateMappingForDocumentUrl(
                getCanonicalUrlFromListItem(filePath), m_session);

            if (m != null)
            {
                mappedPath = TfsUtil.ConcatWithoutDoubleSlashes(m.Target, 
                    getFilePathMapping(m, filePath));
            }

            return mappedPath;
        }

        private static string getFilePathMapping(VersionControlMapping m, string filePath)
        {
            return stripSubsite(m.Source, filePath);
        }

        private void promoteCurrentListToMasterList(MigrationSqlTransaction sqlTrx)
        {
            // prc_iiPromoteCurrentToMaster(@SessionId)
            using (SqlCommand cmd = sqlTrx.CreateCommand())
            {
                cmd.CommandText = "prc_iiPromoteCurrentToMaster";
                cmd.CommandType = CommandType.StoredProcedure;

                cmd.Parameters.Add("@SessionId", SqlDbType.NVarChar).Value = m_session.Id;

                cmd.ExecuteNonQuery();
            }
        }

        bool isMigrationCandidate(DocumentVersion dv)
        {
            bool shouldMigrate = true;

            if (!string.IsNullOrEmpty(dv.Comment))
            {
                if (dv.Comment.Contains(m_commentModifier))
                {
                    shouldMigrate = false;
                }

                if (!string.IsNullOrEmpty(m_skipComment))
                {
                    if (dv.Comment.Contains(m_skipComment))
                    {
                        shouldMigrate = false;
                    }
                }
            }

            return shouldMigrate;
        }

        private string BaseUrl
        {
            get
            {
                return ((WssProvider)m_session.Source).BaseUrl;
            }
        }

        private string DocumentLibrary
        {
            get
            {
                return ((WssProvider)m_session.Source).DocumentLibrary;
            }
        }

        private TimeSpan GmtOffset
        {
            get
            {
                if (!m_gmtOffsetSet)
                {
                    loadGmtOffset();
                }

                return m_gmtOffset;
            }
        }

        private void loadGmtOffset()
        {
            string offset = ((WssProvider)m_session.Source).GmtOffset;
            if (!string.IsNullOrEmpty(offset))
            {
                m_gmtOffset = TimeSpan.Parse(offset);
            }

            m_gmtOffsetSet = true;
        }

        public MigrationDirection Direction
        {
            get
            {
                return MigrationDirection.SourceToTfs;
            }
        }

        ChangeGroupingMananger m_manager;
        VersionControlSession m_session;
        string m_skipComment;
        string m_commentModifier;

        bool m_gmtOffsetSet;
        TimeSpan m_gmtOffset;
    }
}
